﻿using System;
using System.Collections.Generic;
using ValidationFramework.Reflection;
using ValidationFramework.Extensions;
using System.Linq.Expressions;
using ValidationFramework.Configuration;
using System.Configuration;
using ValidationFramework.Configuration.Xml;
using System.Xml.Serialization;
using System.IO;
using ValidationFramework.Configuration.Fluent;
using ValidationFramework.Utilities;

namespace ValidationFramework
{
	/// <summary>
	/// The main static facade for the .Net Validation Framework
	/// </summary>
#if (!SILVERLIGHT)
	[Serializable]
#endif
	public static class Validation
	{
		// --- Fields

		static XmlConfigurationService _xmlConfigurationService;
		static IValidationPolicyFactory _validationPolicyFactory;
		static ReflectionCache _reflectionCache;
		static TypePolicyRepository _typePolicyRepository;
		static MethodPolicyRepository _methodPolicyRepository;
		static ValidationManagerFactory _managerFactory;
        static IApplicationDesciber _AppDesciber;
		static bool _locked;


		// --- Properties

		/// <summary>
		/// Gets or sets the <see cref="IErrorMessageProvider"/> to use.
		/// </summary>
		public static IErrorMessageProvider ErrorMessageProvider
		{
			get;
			private set;
		}
		public static bool LocalizationEnabled { get; private set; }
		internal static bool AttributesEnabled { get; set; }


		// --- Validate | ThrowException

		/// <summary>
		/// Performs validation on a single property. If the property is invalid it will throw an exception.
		/// </summary>
		/// <remarks>
		/// <para>Should be called before the field (representing this property) is set.</para> 
		/// </remarks>
		/// <param name="target">An instance of the object to be validated.</param>
		/// <param name="propertyName">Property to validate. Case sensitive.</param>
		/// <param name="valueToTry">The value of the property being validated.</param>
		/// <param name="ruleSet">The rule set to validate. Use null to validate all rules. Is converted to uppercase.</param>
		/// <param name="context">An <see cref="object"></see> to pass as the context parameter when calling <see cref="Rule.Validate"></see>. Use a null for a non value.</param>
		/// <exception cref="ArgumentNullException">
		/// <paramref name="propertyName"></paramref> is null.</exception>
		/// <exception cref="ArgumentException">
		/// <paramref name="propertyName"></paramref> is <see cref="string.Empty"></see>.</exception>
		/// <exception cref="ArgumentException">No <see cref="PropertyDescriptor"></see> could be found named <paramref name="propertyName"></paramref>.</exception>
		/// <exception cref="ArgumentNullException">
		/// <paramref name="target"></paramref> is null.</exception>
		/// <exception cref="ArgumentException">
		/// <paramref name="ruleSet"></paramref> is a <see cref="string.Empty"></see>.</exception>
		public static void ThrowException(RuntimeTypeHandle targetHandle, string propertyName, object valueToTry, ValidationOptions options)
		{
			Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");
			var typeKey = new TypeKey(targetHandle);
			var propertyKey = new PropertyKey(typeKey, propertyName);

			var policy = _typePolicyRepository.GetPropertyPolicy(propertyKey);

			// TODO: Is this ok? If we want to test the object without actually setting its field 
			// we can only test the member scoped rules. If we wanted to test the parent scoped rules we'd
			// have to set the field. This could have undesired results given the goal of this method. If we
			// could make a copy of the object and set the copies field then it'd be possible. But thats probably
			// not possible.
			var errors = policy.ValidateNoParent(valueToTry, options);

			if (errors.Count > 0)
			{
				throw new ArgumentException(string.Format("Setting property '{0}' to value would result in an invalid state", policy.MemberName), "valueToTry");
			}
		}

        /// <summary>
        /// Validate the parameters of a method. An <see cref="ArgumentException"/> will be thrown on the first invalid parameter.
        /// </summary>
        /// <exception cref="ArgumentException">If any of the <paramref name="parameters"/> is invalid.</exception>
        /// <param name="target">The instance that the method exists on. Null for static types.</param>
        /// <param name="ruleSet">The rule set to validate. Use null to validate all rules. Is converted to uppercase.</param>
        /// <param name="parameters">The values of the parameters to validate. In the same order as they appear in the methods signature.</param>
		/// <param name="methodDelegate">The <see cref="Delegate"/> that represents the method to be validated.</param>
        /// <example>
        /// <code source="Examples\ExampleLibraryCSharp\ParameterValidationManagerExamples\BasicDelegateExample.cs" title="This example shows how to validate the parameters of a method using a RuntimeMethodHandle." lang="cs" region="Example"/>
        /// <code source="Examples\ExampleLibraryVB\ParameterValidationManagerExamples\BasicDelegateExample.vb" title="This example shows how to validate the parameters of a method using a Delegate." lang="vbnet" region="Example"/>
        /// </example>
		/// <exception cref="ArgumentException"><paramref name="ruleSet"/> is a <see cref="string.Empty"/>.</exception>
		/// <exception cref="ArgumentNullException"><paramref name="methodDelegate"/> is null.</exception>
		public static void ThrowException(MethodKey methodKey, ValidationOptions options, params object[] parameters)
		{
			options.ThrowExceptionOnFirstInvalidItem = true;
			ValidateMethodCall(methodKey, options, parameters);
        }

        /// <summary>
        /// Validate the parameters of a method. An <see cref="ArgumentException"/> will be thrown on the first invalid parameter.
        /// </summary>
        /// <exception cref="ArgumentException">If any of the <paramref name="parameters"/> is invalid.</exception>
        /// <param name="target">The instance that the method exists on. Null for static types.</param>
        /// <param name="parameters">The values of the parameters to validate. In the same order as they appear in the methods signature.</param>
		/// <param name="methodDelegate">The <see cref="Delegate"/> that represents the method to be validated.</param>
        /// <example>
        /// <code source="Examples\ExampleLibraryCSharp\ParameterValidationManagerExamples\BasicDelegateExample.cs" title="This example shows how to validate the parameters of a method using a RuntimeMethodHandle." lang="cs" region="Example"/>
        /// <code source="Examples\ExampleLibraryVB\ParameterValidationManagerExamples\BasicDelegateExample.vb" title="This example shows how to validate the parameters of a method using a Delegate." lang="vbnet" region="Example"/>
		/// </example>
		/// <exception cref="ArgumentNullException"><paramref name="methodDelegate"/> is null.</exception>
		public static void ThrowException(MethodKey methodKey, params object[] parameters)
		{
			var options = new ValidationOptions() { ThrowExceptionOnFirstInvalidItem = true };
			ValidateMethodCall(methodKey, options, parameters);
        }

		// --- Validate | Object
		/// <summary>
		/// Performs validation for an object. The default validation options are used
		/// </summary>
		public static IList<ValidationError> Validate(object target)
		{
			return ValidateObject(target.GetType().TypeHandle, target, new ValidationOptions());
		}

		/// <summary>
		/// Performs validation for an object.
		/// </summary>
		/// <param name="target">An instance of the object to be validated.</param>
		/// <param name="context">An <see cref="object"/> to pass as the context parameter when calling <see cref="Rule.Validate"/>. Use a null for a non value.</param>
		/// <exception cref="ArgumentNullException"><paramref name="target"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="ruleSet"/> is a <see cref="string.Empty"/>.</exception>
		/// <returns>All <see cref="ValidationResult"/>s for a given property.</returns>
		public static IList<ValidationError> Validate(object target, ValidationOptions options)
		{
			return ValidateObject(target.GetType().TypeHandle, target, options);
		}

		/// <summary>
		/// Performs validation for a static type.
		/// </summary>
		/// <param name="target">An instance of the object to be validated.</param>
		/// <param name="ruleSet">The rule set to validate. Use null to validate all rules. Is converted to uppercase.</param>
		/// <param name="context">An <see cref="object"/> to pass as the context parameter when calling <see cref="Rule.Validate"/>. Use a null for a non value.</param>
		/// <exception cref="ArgumentNullException"><paramref name="target"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="ruleSet"/> is a <see cref="string.Empty"/>.</exception>
		/// <returns>All <see cref="ValidationResult"/>s for a given property.</returns>
		public static IList<ValidationError> Validate(RuntimeTypeHandle targetHandle, ValidationOptions options)
		{
			return ValidateObject(targetHandle, null, options);
		}

		private static IList<ValidationError> ValidateObject(RuntimeTypeHandle targetHandle, object instance, ValidationOptions options)
		{
			// Get validation policies for all properties in repository
			var typePolicy = _typePolicyRepository.GetOrCreateTypePolicy(new TypeKey(targetHandle));
			var propPolicies = typePolicy.PropertyPolicyDictionary;

			// validate each policy saving errors to the errorlist
			var errors = new List<ValidationError>();
			foreach (var kvp in propPolicies)
			{
				var policy = kvp.Value;

				errors.AddRange(policy.Validate(instance, options));
			}
			return errors;
		}


		// --- Validate | Object | Properties

		/// <summary>
		/// Performs validation for a specific property on a static type.
		/// </summary>
		/// <remarks>
		/// <returns>All <see cref="ValidationError"/>s resulting from validating the given property.</returns>
		public static IList<ValidationError> Validate(RuntimeTypeHandle targetHandle, string propertyName, ValidationOptions options)
		{
			return ValidateProperty(targetHandle, null, propertyName, options);
		}

		/// <summary>
		/// Performs validation for a specific property.
		/// </summary>
		/// <returns>All <see cref="ValidationError"/>s resulting from validating the given property.</returns>
		public static IList<ValidationError> Validate(object target, string propertyName, ValidationOptions options)
		{
			return ValidateProperty(target.GetType().TypeHandle, target, propertyName, options);
		}
#if (!WindowsCE)
		/// <summary>
		/// Validates the specified property on static type.
		/// </summary>
		/// <returns>All <see cref="ValidationError"/>s resulting from validating the given property.</returns>
		public static IList<ValidationError> Validate<TType>(Expression<Func<TType>> property, ValidationOptions options)
		{
			var targetHandle = typeof(TType).TypeHandle;
			var propertyName = ExprUtil.GetMemberName(property);
			return ValidateProperty(targetHandle, null, propertyName, options);
		}

		/// <summary>
		/// Validates the specified property on the given instance.
		/// </summary>
		/// <returns>All <see cref="ValidationError"/>s resulting from validating the given property.</returns>
		public static IList<ValidationError> Validate<TType>(TType instance, Expression<Func<TType>> property, ValidationOptions options)
		{
			var targetHandle = instance.GetType().TypeHandle;
			var propertyName = ExprUtil.GetMemberName(property);
			return ValidateProperty(targetHandle, instance, propertyName, options);
		}
#endif
		private static IList<ValidationError> ValidateProperty(RuntimeTypeHandle targetHandle, object instance, string propertyName, ValidationOptions options)
		{
			var typeKey = new TypeKey(targetHandle);
			var propertyKey = new PropertyKey(typeKey, propertyName);

			var policy = _typePolicyRepository.GetPropertyPolicy(propertyKey);

			return policy.Validate(instance, options);
		}

		// --- Validate | Methods
		
        /// <summary>
        /// Validate the parameters of a method.
        /// </summary>
        /// <param name="target">The instance that the method exists on. Null for static types.</param>
        /// <param name="ruleSet">The rule set to validate. Use null to validate all rules. Is converted to uppercase.</param>
        /// <param name="parameters">The values of the parameters to validate. In the same order as they appear in the methods signature.</param>
        /// <param name="runtimeMethodHandle">The <see cref="RuntimeMethodHandle"/> that represents the method to be validated.</param>
        /// <returns>A <see cref="IList{T}"/> containing all <see cref="ValidationResult"/>s for invalid parameters.</returns>
        /// <exception cref="ArgumentException"><paramref name="ruleSet"/> is a <see cref="string.Empty"/>.</exception>
		public static IList<ValidationError> Validate(MethodKey methodKey, ValidationOptions options, params object[] parameters)
        {
			return ValidateMethodCall(methodKey, options, parameters);
        }

        /// <summary>
        /// Validate the parameters of a method.
        /// </summary>
        /// <param name="target">The instance that the method exists on. Null for static types.</param>
        /// <param name="ruleSet">The rule set to validate. Use null to validate all rules. Is converted to uppercase.</param>
        /// <param name="parameters">The values of the parameters to validate. In the same order as they appear in the methods signature.</param>
        /// <param name="runtimeMethodHandle">The <see cref="RuntimeMethodHandle"/> that represents the method to be validated.</param>
        /// <param name="context">An <see cref="object"/> to pass as the context parameter when calling <see cref="Rule.Validate"/>. Use a null for a non value.</param>
        /// <returns>A <see cref="IList{T}"/> containing all <see cref="ValidationResult"/>s for invalid parameters.</returns>
        /// <exception cref="ArgumentException"><paramref name="ruleSet"/> is a <see cref="string.Empty"/>.</exception>
		public static IList<ValidationError> Validate(MethodKey methodKey, params object[] parameters)
        {
			var options = new ValidationOptions();
			return ValidateMethodCall(methodKey, options, parameters);
        }

        internal static IList<ValidationError> ValidateMethodCall(MethodKey methodKey, ValidationOptions options, object[] parameters)
        {
			var methodPolicy = _methodPolicyRepository.GetOrCreateMethodPolicy(methodKey);
			var call = new MethodCall(methodKey, parameters);
			var errors = new List<ValidationError>();

			errors.AddRange(methodPolicy.Validate(call, options));

			return errors;
        }

		// --- Initialization
		public static void Initialize()
		{
			Validation.Initialize(null);
		}
		public static void Initialize(Action<IInitializationExpression> action)
		{
			lock (typeof(Validation))
			{
				// Initialize internal variables
				_reflectionCache = new ReflectionCache();
				_validationPolicyFactory = new ValidationPolicyFactory(_reflectionCache);
				_typePolicyRepository = new TypePolicyRepository(_validationPolicyFactory, _AppDesciber);
				_methodPolicyRepository = new MethodPolicyRepository(_validationPolicyFactory);
				_managerFactory = new ValidationManagerFactory(_typePolicyRepository, _methodPolicyRepository);
				
				// let consumer secifiy an init expression
				var expression = new InitializationExpression();
				if (action != null)
					action(expression);

				// let the consumer override the default xml readers
				var defaultReaderRepo = new ParserRepository();
				var configReaderRepository = expression.GetParserRepository();
				_xmlConfigurationService = new XmlConfigurationService(configReaderRepository, _reflectionCache, _typePolicyRepository, _methodPolicyRepository);

				// set other consumer specified results
				if (expression.LoadConfigurationFromAppConfig)
				{
					ErrorMessageProvider = GetProviderFromAppConfiguration();
					LoadRulesFromAppConfig(_xmlConfigurationService);
				}
				AttributesEnabled = expression.IgnoreAttributes;
			}
		}


		// --- Configuration | Rules

		public static void ConfigureRulesViaXml(Action<IXmlAddRuleExpression> action)
		{
			// We need a lock when adding rules
			lock (typeof(Validation))
			{
				var expression = new XmlAddRuleExpression(_xmlConfigurationService);
				action(expression);
			}
		}
		public static void ConfigurePolicyFor<TType>(Action<IFluentAddRuleExpression<TType>> action)
		{
			// We need a lock when adding rules
			lock (typeof(Validation))
			{
				var expression = new FluentAddRuleExpression<TType>(_typePolicyRepository);
				action(expression);
			}
		}
		public static void ConfigureRules(Action<IConfigureRuleExpression> action)
		{
			// We need a lock when adding rules
			lock (typeof(Validation))
			{
				var expression = new ConfigureRuleExpression(_typePolicyRepository, _methodPolicyRepository);
				action(expression);
			}
		}


		// --- Analytics | Validation Policies
#if (!WindowsCE)
		public static IValidationPolicy GetValidationPolicy<TType>(Expression<Func<TType, object>> property)
		{
			var typeKey = new TypeKey(typeof(TType).TypeHandle);
			var propertyKey = new PropertyKey(typeKey, ExprUtil.GetMemberNameFromExpression(property));
			return _typePolicyRepository.GetPropertyPolicy(propertyKey);
		}
#endif
		public static IValidationPolicy GetValidationPolicy(PropertyKey key)
		{
			return _typePolicyRepository.GetPropertyPolicy(key);
		}
		public static IValidationPolicy GetValidationPolicy(FieldKey key)
		{
			return _typePolicyRepository.GetFieldPolicy(key);
		}
		public static IValidationPolicy GetValidationPolicy(ParameterKey key)
		{
			return _methodPolicyRepository.GetOrCreateMethodPolicy(key.MethodKey).GetOrCreateParameterPolicy(key);
		}
		public static TypePolicy GetTypePolicyFor<TType>()
		{
			var typeKey = new TypeKey(typeof(TType).TypeHandle);
			return _typePolicyRepository.GetOrCreateTypePolicy(typeKey);
		}
		public static TypePolicy GetTypePolicy(TypeKey key)
		{
			return _typePolicyRepository.GetOrCreateTypePolicy(key);
		}

		public static MemberValidationManager GetPropertyValidationManager(object target)
		{
			// check this on all methods
			if (_managerFactory == null)
				throw new InvalidOperationException("Validation Framework not initialized.");
			return _managerFactory.GetPropertyValidationManager(target);
		}
		public static MemberValidationManager GetFieldValidationManager(object target)
		{
			return _managerFactory.GetFieldValidationManager(target);
		}


		// --- Helper Methods

		private static void LoadRulesFromAppConfig(XmlConfigurationService xmlConfigService)
		{
			var section = ConfigurationManager.GetSection("validationFrameworkConfiguration");
			if (section != null)
			{
				var validationConfigurationSection = (ValidationConfigurationSection)section;
				if (validationConfigurationSection.MappingDocuments != null)
				{
					foreach (MappingDocumentElement mappingDocument in validationConfigurationSection.MappingDocuments)
					{
						xmlConfigService.AddXmlAtUrl(mappingDocument.Url);
					}
				}
			}
		}

		private static IErrorMessageProvider GetProviderFromAppConfiguration()
		{
			var section = ConfigurationManager.GetSection("validationFrameworkConfiguration");
			if (section != null)
			{
				var validationConfigurationSection = (ValidationConfigurationSection)section;
				var errorMessageProviderElement = validationConfigurationSection.ErrorMessageProvider;
				if (errorMessageProviderElement != null)
				{
					var type = Type.GetType(errorMessageProviderElement.TypeName, true);
					var errorMessageProviderXmlSerializer = new XmlSerializer(type);

					ErrorMessageProvider = (IErrorMessageProvider)errorMessageProviderXmlSerializer.Deserialize(new StringReader(errorMessageProviderElement.InnerXml));
				}
			}
			return ErrorMessageProvider;
		}

		private static void Lock()
		{
			_locked = true;
		}

		private static void UnLock()
		{
			_locked = false;
		}
	}
}
